// ====================================================================================
// GLOBALS
// ====================================================================================

// Set debug state to true to print messages
var debug = 1

//The axes
var x_axis_name = "evalA";
var y_axis_name = "evalB";
var makeXScale, makeYScale;
var makeXAxis, makeYAxis;
var x_axis_g, y_axis_g;
var x_axis_l, y_axis_l;

// All the inds in all jobs
// The data is stored in objects
var job_id = -1;
var inds_data = {};
var evals_data = {};

//Other data
var display_labels = false;
var auto_zoom_all = true;

// SVG settings
var svg_margin_bottom = 50;
var svg_margin_left = 70;
var svg_margin_right = 40;
var svg_margin_top = 40;
var label_offset=3;

// Zooming box
var x_zoom = [0,1];
var y_zoom = [0,1];


// ====================================================================================
// HELPER FUNCTIONS
// ====================================================================================

// Debugging
function LOG(msg) {
    if (debug) {console.log(msg);}
}

// Clamp point to be within the graph area
function clamp(position) {
    var x_min = svg_margin_left;
    var x_max = svg_width() - svg_margin_right;
    var y_min = svg_margin_top;
    var y_max = svg_height() - svg_margin_bottom;
    var x = position[0] < x_min ? x_min : (position[0] > x_max ? x_max : position[0]);
    var y = position[1] < y_min ? y_min : (position[1] > y_max ? y_max : position[1]);
    return [x,y];
};

// Width and height functions
function svg_width() {return window.innerWidth - 38;}
function svg_height() {return window.innerHeight - 90;}

// ====================================================================================
// DATA STORAGE OBJECTS
// ====================================================================================

//Stores inds for a job
function JobIndsData() {
    this.inds = [];
    
    // Method to add an ind (will be overwritten if it exists already)
    // The id of the ind must be equal to the position in this.inds
    this.update = update;
    function update(new_inds) {
        LOG("    JobEvalsData.update");
        for (var i=0; i<new_inds.length; i++) {
            var new_ind = new_inds[i];
            var id = new_ind.__id__;
            if (id < this.inds.length) {
                this.inds[id]=new_ind;
            } else {
                for (var i=this.inds.length; i<id; i++) {
                    this.inds.push({}); //this will be replaced later
                }
                this.inds.push(new_ind);
            }
        }
    }
    
    // Method to get an array of all inds
    this.get_inds_array = get_inds_array;
    function get_inds_array() {
        LOG("    JobEvalsData.get_inds_array");
        return this.inds;
    }
    
    // Method to get the number of inds
    this.num_inds = num_inds;
    function num_inds() {
        LOG("    JobEvalsData.num_inds");
        return this.inds.length;
    }
    
    // Method to get a str representation
    this.str = str;
    function str() {
        LOG("    JobEvalsData.str");
        return "    Num inds = " + this.inds.length;
    }
}

// Stores evals for a job
function JobEvalsData() {
    this.evals = {};
    this.eval_names = []; //could use d3.set
    
    // Method to initialise the evaluations
    this._initialise = _initialise;
    function _initialise(first_inds) {
        LOG("    JobEvalsData._initialise");
        for (var i = 0; i < first_inds.length; i++) {
            for (property in first_inds[i]) {
                if (property.charAt(0) != "_") {
                    if (! this.evals.hasOwnProperty(property)) {
                        this.evals[property] = {min:null, max:null};
                        this.eval_names.push(property);
                    }
                }
            }
        }
    }
    
    // Method to update evals from new inds 
    this.update = update;
    function update(new_inds) {
        LOG("    JobEvalsData.update");
        if (this.eval_names.length == 0) {
            this._initialise(new_inds); //this is only done the first time
            updateDropdownOptions();
        }
        for (eval_name in this.evals) { //could use d3.extent
            var values = new_inds.map(function(new_ind){return new_ind[eval_name];});
            values = values.concat([this.evals[eval_name].min, this.evals[eval_name].max]);
            this.evals[eval_name] = {min:d3.min(values), max:d3.max(values)};
        }
    }
    
    // Method to get domain of eval
    this.get_domain = get_domain;
    function get_domain(eval_name) {
        LOG("    JobEvalsData.get_domain");
        var domain;
        if (! this.evals.hasOwnProperty(eval_name)) {
            domain = [0,1];
        } else {
            domain = [this.evals[eval_name].min, this.evals[eval_name].max];
        }
        return domain;
    }
    
    // Method to get an array of all evals
    this.get_evals_array = get_evals_array;
    function get_evals_array() {
        LOG("    JobEvalsData.get_evals_array");
        return this.evals_array;
    }
    
    // Method to get number of evals
    this.num_evals = num_evals;
    function num_evals() {
        LOG("    JobEvalsData.num_evals");
        return this.eval_names.length;
    }
    
    // Method to get a str
    this.str = str;
    function str() {
        LOG("    JobEvalsData.str");
        var result = "    Evals = " + this.eval_names.join();
        for (eval_name in this.evals) {
            result = result + "\n    " + eval_name + " : " + this.evals[eval_name].min + ", " + this.evals[eval_name].max;
        }
        return result;
    }
}

// ====================================================================================
// FUNCTIONS
// ====================================================================================

// Update the dropdown select menus for the axes
// Call when the JobEvalsData object is first initialised
function updateDropdownOptions() {
    LOG("updateDropdownOptions");
    
    //get the option lists
    var x_axis_name_dropdown = document.getElementById('x_axis_name_dropdown');
    var y_axis_name_dropdown = document.getElementById('y_axis_name_dropdown');

    //create options for the drop down
    for (var i=0;i<evals_data[job_id].num_evals();i++) {
        x_axis_name_dropdown.options[i] = new Option(evals_data[job_id].eval_names[i], i);
        y_axis_name_dropdown.options[i] = new Option(evals_data[job_id].eval_names[i], i);
    }
    //make default selections
    x_axis_name_dropdown.selectedIndex = 0;
    y_axis_name_dropdown.selectedIndex = 1; 
    x_axis_name = x_axis_name_dropdown.options[0].text;
    y_axis_name = y_axis_name_dropdown.options[1].text;
    
    LOG("    x_axis_name = " + x_axis_name + ", y_axis_name = " + y_axis_name);
}

// Update svg, axes and sliders
// Call either on a window resize or on_inds_updated, on_job_changed
function updateResize() {
    LOG("updateResize");
    
    // SVG: Resize
    d3.select("#svg").
        attr("width", svg_width()).
        attr("height", svg_height());
        
    // Axes: Ranges functions
    function x_range() {return [svg_margin_left, svg_width() - svg_margin_right];}
    function y_range() {return [svg_height() - svg_margin_bottom, svg_margin_top];}
    
    // Axes: Domains
    if (auto_zoom_all) {
        x_zoom = evals_data[job_id].get_domain(x_axis_name);
        y_zoom = evals_data[job_id].get_domain(y_axis_name);
    }
    
    // Axes: x and y scale functions
    makeXScale = d3.scale.linear().domain(x_zoom).range(x_range());
    makeYScale = d3.scale.linear().domain(y_zoom).range(y_range());
    
    // Axes: x and y axis functions
    makeXAxis.scale(makeXScale);
    makeYAxis.scale(makeYScale);
    
    // Update the axes
    x_axis_g.
        attr("transform", "translate( 0 ," + (svg_height() - svg_margin_bottom) +")").
        call(makeXAxis);
    y_axis_g.
        attr("transform", "translate(" + svg_margin_left + ", 0 )").
        call(makeYAxis);
    
    // Update the axes labels
    x_axis_l.
        attr("x", svg_width()/2 ).
        attr("y", svg_height() -10 ).
        text(x_axis_name);
    y_axis_l.
        attr("x", 0 - (svg_height()/2) ).
        attr("y", 10).
        text(y_axis_name);
    
    // Slider: regenerate, makeSlider(target, min, max, on_finish, on_change)
    // d3.select("#id_slider svg").remove();
    // makeSlider(d3.select("#id_slider").node(), svg_width(), 0, inds_data[job_id].num_inds(), sliderOnChange, sliderOnFinish);
    
    LOG("    x domain = " + x_zoom + "  >>  x range = " + x_range());
    LOG("    y domain = " + y_zoom + "  >>  y range = " + y_range());

}

// Delete all the points (circles and labels)
// Call on_job_changed
function deletePoints() {
    LOG("deletePoints");
    LOG(inds_data[job_id].str());
    
    // Get the data
    data_array = inds_data[job_id].get_inds_array();
    var d3_circles = d3.select("#graph").selectAll("circle.point").data(data_array); //, function(ind) {return ind.__id__;});
    var d3_labels = d3.select("#graph").selectAll("text.point").data(data_array) //, function(ind) {return ind.__id__;});
    
    // Job changed, remove everything
    d3_circles.remove();
    d3_labels.remove();

}

// Redraw the graph (points, point labels, axes, axes labels)
// Call any time the graph changes - window resize, on_inds_updated, on_job_changed
function updateGraph(transition_duration) {
    LOG("updateGraph");
    LOG(inds_data[job_id].str());
    
    // Function to hide points with missing evaluations
    // So points with missing data will be hidden. Later when the data is updated, they will be 
    // shown (via the 'UPDATE' circles).
    function filter_data(ind) {
        if (! ind.hasOwnProperty(x_axis_name) || ! ind.hasOwnProperty(y_axis_name)){
            return "point hide"; 
        } else if (ind[x_axis_name] < x_zoom[0] || ind[x_axis_name] > x_zoom[1] ) {
            return "point hide"; 
        } else if (ind[y_axis_name] < y_zoom[0] || ind[y_axis_name] > y_zoom[1] ) {
            return "point hide"; 
        }
        return "point show";

    }
    
    // Get the data
    data_array = inds_data[job_id].get_inds_array();
    var d3_circles = d3.select("#graph").selectAll("circle.point").data(data_array); //, function(ind) {return ind.__id__;});
    var d3_labels = d3.select("#graph").selectAll("text.point").data(data_array); //, function(ind) {return ind.__id__;});
    
    // UPDATE circles selection
    d3_circles.transition().duration(transition_duration).
        attr("class", filter_data).
        attr("cx",function(ind) {return makeXScale(ind[x_axis_name]);}).
        attr("cy",function(ind) {return makeYScale(ind[y_axis_name]);}).
        attr("r", function(ind) {if (ind._inact_time == null) { return 3;} else {return 2;}}).
        attr("fill", function(ind) {if (ind._inact_time == null) { return "blue";} else {return "black";}}); 
    // ENTER circles selection
    d3_circles.enter().append("circle").
        attr("class", filter_data).
        attr("cx",function(ind) {return makeXScale(ind[x_axis_name]);}).
        attr("cy",function(ind) {return makeYScale(ind[y_axis_name]);}).
        attr("r", function(ind) {if (ind._inact_time == null) { return 3;} else {return 2;}}).
        attr("fill", function(ind) {if (ind._inact_time == null) { return "blue";} else {return "black";}}); 
    
    
    if (display_labels) {
        // UPDATE labels selection
        d3_labels.transition().duration(transition_duration).
            attr("class", filter_data).                    
            attr("x",function(ind) {return makeXScale(ind[x_axis_name]) + label_offset;}).
            attr("y",function(ind) {return makeYScale(ind[y_axis_name]) - label_offset;}).
            attr("fill", "black"); 
        // ENTER labels selection
        d3_labels.enter().append("text").
            attr("class", filter_data).
            attr("x",function(ind) {return makeXScale(ind[x_axis_name]) + label_offset;}).
            attr("y",function(ind) {return makeYScale(ind[y_axis_name]) - label_offset;}).
            attr("fill", "black").
            text(function(ind) {return ind.__id__;}); 
    } else {
        d3_labels.remove();
        d3_labels.exit().remove();
    }
}

// Redraw the zoom rectangle
function updateZoomRectange(zoom_mousedown, zoom_mouseup) {
    var x = d3.min([zoom_mousedown[0], zoom_mouseup[0]]);
    var y = d3.min([zoom_mousedown[1], zoom_mouseup[1]]);
    var width = Math.abs(zoom_mousedown[0] - zoom_mouseup[0]);
    var height = Math.abs(zoom_mousedown[1] - zoom_mouseup[1]);

    d3.select('#zoom_rect').
        attr("x", x).
        attr("y", y).
        attr("width", width).
        attr("height", height);
}

// Create slider
//https://gist.github.com/dfm/3709760
function makeSlider(target, width, min, max, on_finish, on_change) {
    var _this = this;
    var w = width - 20, h = 20, margin = 10, radius = 5;
    if (max < 0) {max=100;}

    // On change event.
    this.on_change = function () {};
    if (typeof(on_change) !== "undefined") this.on_change = on_change;

    // And the on finish function.
    this.on_finish = function () {};
    if (typeof(on_finish) !== "undefined") this.on_finish = on_finish;

    // Set up the scale.
    this.min = min;
    this.max = max;
    this.current_min = min;
    this.current_max = max;
    this.scale = d3.scale.linear().range([radius,w - radius]).domain([min, max]);
    
    LOG("test 0 = " + this.scale(0));
    LOG("test 50 = " + this.scale(50));
    LOG("test 100 = " + this.scale(100));
    
    // Select the target element.
    this.el = d3.select(target)
                .append("svg:svg")
                    .attr("width", w + 2 * margin)
                    .attr("height", h)
                .append("svg:g")
                    .attr("transform", "translate(" + margin + ", " + 0 + ")");

    // Draw the bar.
    this.bar = this.el.append("line")
                    .attr("class","slider_line")
                    .attr("y1", 0.5 * (h - 1))
                    .attr("y2", 0.5 * (h - 1))
                    .attr("x1", this.scale(min) - radius)
                    .attr("x2", this.scale(max) + radius);

    // Set up the buttons.
    this.selected = undefined;
    this.buttons = this.el.selectAll("circle.slider_button")
                    .data([{order: -1, pos: min}, {order: 1, pos: max}])
                    .enter()
                        .append("circle")
                            .attr("class","slider_button")
                            .attr("cx", function (d) {
                                return _this.scale(d.pos) + d.order * radius;
                            })
                            .attr("cy", 0.5 * (h - 1))
                            .attr("r", 5)
                            .attr("order", function (d) { return d.order; })
                            .on("mousedown", function () {
                                _this.selected = d3.select(this);
                            });

    // Body-wide event listeners.
    d3.select("body").on("mousemove", function () {
        if (typeof(_this.selected) !== "undefined") {
            // Find the value implied by the mouse position.
            var value = _this.scale.invert(d3.mouse(_this.el[0][0])[0]);

            // Bound the motion of the buttons.
            var x, order = _this.selected.attr("order");
            if (order == 1) {
                x = Math.min(_this.max, Math.max(_this.current_min, value));
                _this.current_max = x;
            } else {
                x = Math.min(_this.current_max, Math.max(_this.min, value));
                _this.current_min = x;
            }

            // Move the button sprite.
            _this.selected.attr("cx", _this.scale(x)
                                + _this.selected.attr("order") * radius);

            // Fire the change event.
            _this.on_change([_this.current_min, _this.current_max]);
        }
    }).on("mouseup", function () {
        // Clear the selection.
        _this.selected = undefined;

        _this.on_finish([_this.current_min, _this.current_max]);
    });
}

// Event listeners for slider
function sliderOnChange() {
    LOG("sliderOnChange");
}
function sliderOnFinish() {
    LOG("sliderOnFinish");
}

// ====================================================================================
// DEXEN EVENTS
// ====================================================================================

// DEXEN EVENT: Get the updated inds
function on_inds_updated(job_info, new_inds) {
LOG("==================================================");
    LOG("on_inds_updated...");
    
    // Check that we have some inds
    if (!  new_inds) {LOG("No new_inds found..."); return;}
    
    // Update the inds (add new inds to array)
    inds_data[job_id].update(new_inds);
    
    // Update the evals (min amd max)
    evals_data[job_id].update(new_inds);
    
    // Update
    updateResize();
    updateGraph(1000);
    
    LOG("All jobs:");
    for (job_id in inds_data) {
        LOG("    " + inds_data[job_id].str())
    }
    
}

//  DEXEN EVENT: Job changed
function cur_job_changed(job_info) {
    LOG("==================================================");
    LOG("cur_job_changed...");
    
    // Delet all existing points (circles and labels)
    deletePoints();
    
    //Check we have a job
    if (job_info == null) {
        job_id = -1;
        d3.select("#job_id").text("(No job selected.)");
    } else {
        job_id = job_info.job_id;
        d3.select("#job_id").text("Job ID: " + job_id);
    }
    
    // Create new objects for new job
    if (! job_id in inds_data || ! inds_data[job_id]) {
        inds_data[job_id] = new JobIndsData();
    }
    if (! job_id in evals_data || ! evals_data[job_id]) {
        evals_data[job_id] = new JobEvalsData();
    } 
    
    // Update
    updateResize();
    updateGraph(0);
    
    LOG("    New Job ID is " + job_id);
}

// ====================================================================================
// UI EVENTS
// ====================================================================================

// UI EVENT: onload
// d3.select("body").on("load", function() {
// $(document).ready(function() {
window.onload=function(){

    LOG("document ready");

    // Create objects to store dummy data
    inds_data[-1] = new JobIndsData();
    evals_data[-1] = new JobEvalsData();
    
    // Create the two functions that generate the axes
    makeXAxis = d3.svg.axis().orient("bottom");
    makeYAxis = d3.svg.axis().orient("left");
    
    // Create SVG elements (add all attrs that do not change)
    var svg_container = d3.select("#graph").append("svg").
        attr("id", "svg").
        style("background", "#ffffff").
        append("g").
        attr("id", "graph");
    svg_container.append("svg:rect").
        attr("id", "zoom_rect").
        attr("fill-opacity", 0).
        attr("stroke", "black").
        attr("stroke-width", 1).
        attr("visibility","hidden");
    x_axis_g = svg_container.append("g").
        attr("class", "x axis").
        call(makeXAxis);
    y_axis_g = svg_container.append("g").
        attr("class", "y axis").
        call(makeYAxis);
    x_axis_l = svg_container.append("text").
        attr("class", "axis label").
        attr("text-anchor", "middle");
    y_axis_l = svg_container.append("text").
        attr("class", "axis label").
        attr("transform", "rotate(-90)").
        attr("text-anchor", "middle").
        attr("dy", "1em");
    svg_group = svg_container.append("g").
        attr("id", "graph");

    // Add a slider
    makeSlider(d3.select("#id_slider").node(), svg_width() - 172, 0, 100, sliderOnChange, sliderOnFinish);
        
    // Set the defaults for check boxes
    d3.select("#show_id_checkbox").attr("checked", false);
    d3.select("#auto_zoom_all_checkbox").attr("checked", true);
    
    // Update sizes to window size
    updateResize();
    
    // Update graph (append to svg_group)
    updateGraph(0);
    
    // Get already loaded inds
    if (typeof(dexen) !== 'undefined') {
        LOG("    dexen.get_all_inds()");
        
        var cur_job = $.parseJSON(dexen.get_cur_job()); //<<<<<<<<<<<<<<<
        cur_job_changed(cur_job);

        var inds = $.parseJSON(dexen.get_all_inds()); //<<<<<<<<<<<<<<<
        for (var i = 0; i < inds.length; i++) {
            on_inds_updated(inds[i].job_info, inds[i].inds);
        }
    }
    
    // WINDOW EVENT: resize
    $(window).resize(function(){
        LOG("window resize");

        //Refresh
        updateResize();
        updateGraph(0);
    });
    
    // CHECKBOX EVENT: click
    d3.select("#display_id_checkbox").on("click", function(){
        LOG("display_labels_checkbox click, " + this.checked);
        
        if (this.checked) {
            display_labels=true;
        } else {
            display_labels=false;
        }
        updateGraph(0);
    });
    
    // CHECKBOX EVENT: click
    d3.select("#auto_zoom_all_checkbox").on("click", function(){
        LOG("auto_zoom_all_checkbox click, " + this.checked);
        
        if (this.checked) {
            auto_zoom_all=true;
        } else {
            auto_zoom_all=false;
        }
        updateResize();
        updateGraph(0);
    });
    
    // BUTTON EVENT: click
    d3.select("#zoom_all").on("click",function(){
        LOG("zoom_all click");
        
        // Hide the zoom box
        d3.select("#zoom_rect").attr("visibility","hidden");
        
        // Reset
        x_zoom = evals_data[job_id].get_domain(x_axis_name);
        y_zoom = evals_data[job_id].get_domain(y_axis_name);
        
        updateResize();
        updateGraph(1000);
    });
    
    // SELECT EVENT: change (Dropdown selection event)
    d3.select("#x_axis_name_dropdown").on("change", function() {
        LOG("x_axis_name_dropdown change");

        // Set the x axis
        x_axis_name = $('#x_axis_name_dropdown option:selected').text();
        //Do a refresh
        updateResize();
        updateGraph(1000);
    });
    
    // SELECT EVENT: change (Dropdown selection event)
    d3.select("#y_axis_name_dropdown").on("change", function() {
        LOG("y_axis_name_dropdown change");

        // Set the y axis
        y_axis_name = $('#y_axis_name_dropdown option:selected').text();
        //Do a refresh
        updateResize();
        updateGraph(1000);
    });
    
    // MOUSE EVENT: mousedown
    var zoom_mousedown;
    d3.select("#svg").on("mousedown", function() {
      LOG("mousedown");
      d3.select("#zoom_rect").attr("visibility","visible");
      zoom_mousedown = clamp(d3.mouse(d3.select('#svg').node()));
    });
    
    // MOUSE EVENT: mouseup
    d3.select("#svg").on("mouseup", function() {
        LOG("mouseup");
        var zoom_mouseup = clamp(d3.mouse(d3.select('#svg').node()));
        updateZoomRectange(zoom_mousedown, zoom_mouseup);
        
        //set the zoom
        x_zoom = [makeXScale.invert(zoom_mousedown[0]), makeXScale.invert(zoom_mouseup[0])].sort(d3.ascending);
        y_zoom = [makeYScale.invert(zoom_mousedown[1]), makeYScale.invert(zoom_mouseup[1])].sort(d3.ascending);
        zoom_mousedown = null;
        
        // Update
        updateResize();
        updateGraph(1000);
        
        // Hide the rect
        d3.select("#zoom_rect").attr("visibility","hidden");
    });
    
    // MOUSE EVENT: mousemove
    d3.select("#svg").on("mousemove", function() {
        //LOG("mouseover");
        var zoom_mouse = clamp(d3.mouse(d3.select('#svg').node()));
        //d3.select("#mousepos").html(mouse[0] +', '+ mouse[1] +" : "+ makeXScale.invert(mouse[0]) +', '+ makeYScale.invert(mouse[1]));
        if (zoom_mousedown != null) {
            updateZoomRectange(zoom_mousedown, zoom_mouse);
        }
    });
}








